This commit is contained in:
Andre
2026-10-22 21:00:05 +02:00
commit 9c278f49b6
20 changed files with 909 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
* text=auto
# Laufen auf dem Pi: CRLF wuerde Shell-Skript und systemd-Unit brechen
*.sh text eol=lf
*.service text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
# Batch-Dateien unter Windows immer mit CRLF
*.cmd text eol=crlf
+23
View File
@@ -0,0 +1,23 @@
# Enthalten das Tunnel-Secret
client/config.ps1
server/restrictions.yaml
# Installationsspezifisch
server/.env
# Wird beim ersten Start von GitHub geladen
client/bin/
# Laufzeitdaten
client/logs/
client/chrome-profile/
# Interne Notizen (Heim-IP, Domain, NPM-Details)
notes/
# Editor und Windows
.vscode/
.idea/
Thumbs.db
desktop.ini
*.bak
+127
View File
@@ -0,0 +1,127 @@
# Tunnler
[English](#english) | [Deutsch](#deutsch)
---
## English
> **Disclaimer:** This tool was vibe coded with Claude Code — architecture and structure were planned together with AI, and all implementation was done through prompts. Not a single line was written by hand.
Routes only Chrome and Codex through a wstunnel server on a home Raspberry Pi, even behind a corporate proxy that blocks sites.
Unlike a web proxy, the tunnel passes raw TCP connections. TLS ends at the real server, so Google and ChatGPT logins work.
### Features
- Local proxy on `127.0.0.1:1080`, used only by a dedicated Chrome profile and Codex
- Tunnel as WebSocket over `wss://` on port 443, optionally through a corporate proxy
- Access only via a secret URL path
- Server allows only TCP 80/443 to public destinations, the home network is blocked
- Client needs no admin rights and runs on Windows PowerShell 5.1
### Installation
Prerequisites:
- Raspberry Pi with Docker and Nginx Proxy Manager, own domain with SSL certificate
- Windows 10/11 with Google Chrome
Step-by-step guide for server and client (German): [docs/installation.md](docs/installation.md)
### Usage
Double-click `client\Chrome-Tunnel.cmd`. The tunnel starts hidden in the background, then a separate Chrome window opens that runs entirely through the Pi.
Run `client\PATH-eintragen.cmd` once. Afterwards type `codexx` instead of `codex` in any project folder, for example in the VS Code terminal. Arguments are passed through.
`Tunnel-Start.cmd` starts only the tunnel. Then set `127.0.0.1:1080` as HTTP proxy in any program. `Tunnel-Status.cmd` shows the public IP through the tunnel. `Tunnel-Stop.cmd` stops the tunnel.
### Configuration
All client values live in `client\config.ps1` (copy from `config.example.ps1`).
| Variable | Meaning | Default |
|---|---|---|
| `$TunnelSecret` | Secret path, identical to the server | empty |
| `$TunnelServer` | Server address | `wss://deine-domain.de:443` |
| `$LocalProxy` | Local proxy for Chrome and Codex | `127.0.0.1:1080` |
| `$UpstreamProxy` | Corporate proxy, empty = direct | empty |
| `$UpstreamProxyUser` / `$UpstreamProxyPassword` | Corporate proxy login | empty |
| `$SkipTlsVerify` | Disable certificate check, only if the proxy breaks TLS | `$false` |
After a change, run `client\Tunnel-Stop.cmd` and start the tunnel again.
### Known blockers
- On a corporate network the tunnel times out without `$UpstreamProxy`. See [corporate proxy](docs/installation.md#teil-3--einsatz-hinter-einem-firmen-proxy).
- The first start downloads `wstunnel.exe` from GitHub. Without access to github.com, place it in `client\bin\` manually.
- Private addresses such as `192.168.x.x` are blocked through the tunnel. The client log then shows `Invalid status code: 400`. Add them to the program's proxy exceptions.
- `client\config.ps1` and `server/restrictions.yaml` contain the secret. Do not share or commit them.
### Further documentation
- [docs/installation.md](docs/installation.md) — installation, corporate proxy, rotating the secret, updating, troubleshooting (German)
- [wstunnel](https://github.com/erebe/wstunnel) — the tunnel software used
---
## Deutsch
> **Hinweis:** Dieses Tool wurde mit Claude Code vibe coded — Architektur und Struktur wurden gemeinsam mit KI geplant, die gesamte Implementierung erfolgte per Prompt. Keine einzige Zeile wurde von Hand geschrieben.
Leitet nur Chrome und Codex über einen wstunnel-Server auf dem Raspberry Pi zu Hause ins Internet, auch hinter einem Firmen-Proxy, der Seiten sperrt.
Anders als ein Web-Proxy reicht der Tunnel reine TCP-Verbindungen durch. TLS endet beim echten Server, daher funktionieren Google- und ChatGPT-Logins.
### Funktionen
- Lokaler Proxy auf `127.0.0.1:1080`, den nur ein eigenes Chrome-Profil und Codex nutzen
- Tunnel als WebSocket über `wss://` auf Port 443, optional durch einen Firmen-Proxy
- Zugang nur mit geheimem URL-Pfad
- Server erlaubt nur TCP 80/443 zu öffentlichen Zielen, das Heimnetz ist gesperrt
- Client ohne Admin-Rechte, läuft mit Windows PowerShell 5.1
### Installation
Voraussetzungen:
- Raspberry Pi mit Docker und Nginx Proxy Manager, eigene Domain mit SSL-Zertifikat
- Windows 10/11 mit Google Chrome
Schritt-für-Schritt-Anleitung für Server und Client: [docs/installation.md](docs/installation.md)
### Nutzung
`client\Chrome-Tunnel.cmd` doppelklicken. Der Tunnel startet unsichtbar im Hintergrund, danach öffnet sich ein eigenes Chrome-Fenster, das komplett über den Pi läuft.
Einmal `client\PATH-eintragen.cmd` ausführen. Danach in jedem Projektordner `codexx` statt `codex` eingeben, zum Beispiel im VS-Code-Terminal. Argumente werden durchgereicht.
`Tunnel-Start.cmd` startet nur den Tunnel. Danach `127.0.0.1:1080` in beliebigen Programmen als HTTP-Proxy eintragen. `Tunnel-Status.cmd` zeigt die öffentliche IP über den Tunnel. `Tunnel-Stop.cmd` beendet den Tunnel.
### Konfiguration
Alle Client-Werte stehen in `client\config.ps1` (Vorlage: `config.example.ps1`).
| Variable | Bedeutung | Standard |
|---|---|---|
| `$TunnelSecret` | Geheimer Pfad, identisch mit dem Server | leer |
| `$TunnelServer` | Adresse des Servers | `wss://deine-domain.de:443` |
| `$LocalProxy` | Lokaler Proxy für Chrome und Codex | `127.0.0.1:1080` |
| `$UpstreamProxy` | Firmen-Proxy, leer = direkt | leer |
| `$UpstreamProxyUser` / `$UpstreamProxyPassword` | Anmeldung am Firmen-Proxy | leer |
| `$SkipTlsVerify` | Zertifikatsprüfung abschalten, nur bei TLS-Aufbruch | `$false` |
Nach einer Änderung `client\Tunnel-Stop.cmd` ausführen und den Tunnel neu starten.
### Bekannte Stolpersteine
- Im Firmennetz läuft der Tunnel ohne `$UpstreamProxy` in einen Timeout. Siehe [Einsatz hinter einem Firmen-Proxy](docs/installation.md#teil-3--einsatz-hinter-einem-firmen-proxy).
- Der erste Start lädt `wstunnel.exe` von GitHub. Ohne Zugriff auf github.com die Datei von Hand nach `client\bin\` legen.
- Private Adressen wie `192.168.x.x` sind über den Tunnel gesperrt. Das Client-Log zeigt dann `Invalid status code: 400`. Solche Adressen im Programm als Proxy-Ausnahme eintragen.
- `client\config.ps1` und `server/restrictions.yaml` enthalten das Secret. Nicht weitergeben und nicht committen.
### Weitere Dokumentation
- [docs/installation.md](docs/installation.md) — Installation, Firmen-Proxy, Secret tauschen, Aktualisieren, Fehlerbehebung
- [wstunnel](https://github.com/erebe/wstunnel) — verwendete Tunnel-Software
+3
View File
@@ -0,0 +1,3 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\chrome.ps1"
if errorlevel 1 pause
+3
View File
@@ -0,0 +1,3 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\path.ps1"
pause
+3
View File
@@ -0,0 +1,3 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\tunnel.ps1" start
pause
+3
View File
@@ -0,0 +1,3 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\tunnel.ps1" status
pause
+3
View File
@@ -0,0 +1,3 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\tunnel.ps1" stop
pause
+2
View File
@@ -0,0 +1,2 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\codex.ps1" %*
+19
View File
@@ -0,0 +1,19 @@
# Als config.ps1 speichern und ausfuellen.
# config.ps1 enthaelt das Tunnel-Secret: nicht weitergeben, nicht committen.
# Geheimer Pfad, identisch mit server/restrictions.yaml und der NPM-Location
$TunnelSecret = ''
$TunnelServer = 'wss://deine-domain.de:443'
# Lokaler Proxy, auf den Chrome und Codex zeigen
$LocalProxy = '127.0.0.1:1080'
# Firmen-Proxy aus der PAC-Datei, z. B. 'http://proxy.firma.de:8080'.
# Leer = direkte Verbindung (zu Hause, Hotspot).
$UpstreamProxy = ''
$UpstreamProxyUser = ''
$UpstreamProxyPassword = ''
# Nur auf $true setzen, wenn der Firmen-Proxy TLS aufbricht
# (Zertifikat der eigenen Domain im Browser nicht von Let's Encrypt).
$SkipTlsVerify = $false
+19
View File
@@ -0,0 +1,19 @@
# Startet den Tunnel (falls noetig) und ein eigenes Chrome-Profil, das komplett
# ueber den Tunnel laeuft. Normales Chrome und alle anderen Programme bleiben direkt.
. "$PSScriptRoot\common.ps1"
Start-Tunnel
$chrome = @(
"$env:ProgramFiles\Google\Chrome\Application\chrome.exe",
"${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe",
"$env:LOCALAPPDATA\Google\Chrome\Application\chrome.exe"
) | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $chrome) { throw "chrome.exe nicht gefunden." }
$profileDir = Join-Path $ClientDir 'chrome-profile'
Start-Process -FilePath $chrome -ArgumentList @(
"--proxy-server=$ProxyUrl",
"--user-data-dir=`"$profileDir`"",
'--no-first-run'
)
+17
View File
@@ -0,0 +1,17 @@
# Startet den Tunnel (falls noetig) und Codex mit Proxy-Variablen nur fuer diesen Aufruf.
# Alle Argumente werden an codex durchgereicht.
. "$PSScriptRoot\common.ps1"
Start-Tunnel
$names = 'HTTPS_PROXY', 'HTTP_PROXY', 'NO_PROXY'
$saved = @{}
foreach ($n in $names) { $saved[$n] = [Environment]::GetEnvironmentVariable($n, 'Process') }
try {
$env:HTTPS_PROXY = $ProxyUrl
$env:HTTP_PROXY = $ProxyUrl
$env:NO_PROXY = 'localhost,127.0.0.1'
& codex @args
} finally {
foreach ($n in $names) { [Environment]::SetEnvironmentVariable($n, $saved[$n], 'Process') }
}
+108
View File
@@ -0,0 +1,108 @@
# Gemeinsame Funktionen fuer die Tunnel-Skripte. Wird per Dot-Sourcing geladen.
$ErrorActionPreference = 'Stop'
$ClientDir = Split-Path $PSScriptRoot -Parent
$ConfigPath = Join-Path $ClientDir 'config.ps1'
if (-not (Test-Path $ConfigPath)) {
throw "config.ps1 fehlt. config.example.ps1 nach config.ps1 kopieren und ausfuellen."
}
. $ConfigPath
$WstunnelVersion = '11.1.0' # passend zum Server-Image in server/docker-compose.yml
$Exe = Join-Path $ClientDir 'bin\wstunnel.exe'
$LogDir = Join-Path $ClientDir 'logs'
$ProxyUrl = "http://$LocalProxy"
function Get-TunnelArgs([string]$LogLevel) {
$tunnelArgs = @('client', '-L', "http://$LocalProxy", '-c', '2', '--log-lvl', $LogLevel)
if (-not $SkipTlsVerify) { $tunnelArgs += '--tls-verify-certificate' }
if ($UpstreamProxy) { $tunnelArgs += @('--http-proxy', $UpstreamProxy) }
if ($UpstreamProxyUser) { $tunnelArgs += @('--http-proxy-login', $UpstreamProxyUser) }
$tunnelArgs += $TunnelServer
return $tunnelArgs
}
# Secret und Proxy-Passwort gehen per Umgebung an wstunnel, damit sie nicht in
# der Kommandozeile stehen (Prozesslisten, EDR-Protokolle). HTTP_PROXY wird
# entfernt, weil wstunnel es sonst als Upstream-Proxy uebernimmt. NO_COLOR liest
# wstunnel ebenfalls und akzeptiert nur true/false (ein gesetztes NO_COLOR=1 bricht den Start).
function Invoke-WithTunnelEnv([scriptblock]$Action) {
$names = 'WSTUNNEL_HTTP_UPGRADE_PATH_PREFIX', 'WSTUNNEL_HTTP_PROXY_PASSWORD', 'HTTP_PROXY', 'NO_COLOR'
$saved = @{}
foreach ($n in $names) { $saved[$n] = [Environment]::GetEnvironmentVariable($n, 'Process') }
try {
$env:NO_COLOR = 'true'
$env:WSTUNNEL_HTTP_UPGRADE_PATH_PREFIX = $TunnelSecret
if ($UpstreamProxyPassword) { $env:WSTUNNEL_HTTP_PROXY_PASSWORD = $UpstreamProxyPassword }
Remove-Item Env:HTTP_PROXY -ErrorAction SilentlyContinue
& $Action
} finally {
foreach ($n in $names) { [Environment]::SetEnvironmentVariable($n, $saved[$n], 'Process') }
}
}
function Test-TunnelListening {
$parts = $LocalProxy.Split(':')
$client = New-Object System.Net.Sockets.TcpClient
try { $client.Connect($parts[0], [int]$parts[1]); return $true }
catch { return $false }
finally { $client.Close() }
}
function Get-TunnelProcess {
Get-Process wstunnel -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $Exe }
}
# wstunnel.exe liegt nicht im Repo. Beim ersten Start wird das offizielle Release
# geladen und gegen die checksums.txt des Releases geprueft.
function Install-Wstunnel {
if (Test-Path $Exe) { return }
$base = "https://github.com/erebe/wstunnel/releases/download/v$WstunnelVersion"
$archiveName = "wstunnel_${WstunnelVersion}_windows_amd64.tar.gz"
$tmp = Join-Path ([IO.Path]::GetTempPath()) "tunnler-$([guid]::NewGuid())"
New-Item -ItemType Directory -Force $tmp | Out-Null
$oldProgress = $ProgressPreference
try {
Write-Host "Lade wstunnel $WstunnelVersion ..."
$ProgressPreference = 'SilentlyContinue' # sonst ist Invoke-WebRequest in PS 5.1 sehr langsam
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$archive = Join-Path $tmp $archiveName
$sums = Join-Path $tmp 'checksums.txt'
Invoke-WebRequest "$base/$archiveName" -OutFile $archive -UseBasicParsing
Invoke-WebRequest "$base/checksums.txt" -OutFile $sums -UseBasicParsing
$line = Get-Content $sums | Where-Object { $_ -match "\s$([regex]::Escape($archiveName))$" }
if (-not $line) { throw "$archiveName fehlt in checksums.txt." }
if ((Get-FileHash $archive -Algorithm SHA256).Hash -ne ($line -split '\s+')[0]) {
throw "Pruefsumme von $archiveName stimmt nicht. Download verworfen."
}
& "$env:SystemRoot\System32\tar.exe" -xzf $archive -C $tmp wstunnel.exe
if ($LASTEXITCODE -ne 0) { throw "Entpacken von $archiveName fehlgeschlagen." }
New-Item -ItemType Directory -Force (Split-Path $Exe) | Out-Null
Move-Item (Join-Path $tmp 'wstunnel.exe') $Exe
} finally {
$ProgressPreference = $oldProgress
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
# Startet den Client unsichtbar im Hintergrund, falls er noch nicht laeuft.
function Start-Tunnel {
if (-not $TunnelSecret) { throw "TunnelSecret in config.ps1 ist leer." }
if (Test-TunnelListening) { return }
Install-Wstunnel
New-Item -ItemType Directory -Force $LogDir | Out-Null
$log = Join-Path $LogDir 'wstunnel.log'
Invoke-WithTunnelEnv {
Start-Process -FilePath $Exe -ArgumentList (Get-TunnelArgs 'WARN') -WindowStyle Hidden `
-RedirectStandardOutput $log -RedirectStandardError (Join-Path $LogDir 'wstunnel.err.log')
}
for ($i = 0; $i -lt 20; $i++) {
if (Test-TunnelListening) { return }
Start-Sleep -Milliseconds 250
}
throw "Tunnel-Client lauscht nicht auf $LocalProxy. Details: $log"
}
function Stop-Tunnel {
Get-TunnelProcess | Stop-Process -Force
}
+25
View File
@@ -0,0 +1,25 @@
# Traegt den Client-Ordner in den Benutzer-PATH ein, damit 'codexx' in jedem Terminal geht.
# Der PATH wird roh aus der Registry gelesen, damit Eintraege wie %USERPROFILE%\... nicht
# beim Zurueckschreiben fest aufgeloest werden.
$ErrorActionPreference = 'Stop'
$ClientDir = Split-Path $PSScriptRoot -Parent
$key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true)
try {
$raw = $key.GetValue('Path', '', 'DoNotExpandEnvironmentNames')
if (($raw -split ';' | ForEach-Object { $_.TrimEnd('\') }) -contains $ClientDir.TrimEnd('\')) {
Write-Host "$ClientDir steht schon im PATH."
return
}
$key.SetValue('Path', (($raw.TrimEnd(';') + ";$ClientDir").TrimStart(';')), 'ExpandString')
} finally {
$key.Close()
}
# Explorer ueber die Aenderung informieren, sonst sehen neu gestartete Programme sie erst nach einer Neuanmeldung
Add-Type -Namespace Tunnler -Name Native -MemberDefinition '[DllImport("user32.dll", CharSet=CharSet.Auto)] public static extern IntPtr SendMessageTimeout(IntPtr h, uint m, UIntPtr w, string l, uint f, uint t, out UIntPtr r);'
$result = [UIntPtr]::Zero
[Tunnler.Native]::SendMessageTimeout([IntPtr]0xffff, 0x1A, [UIntPtr]::Zero, 'Environment', 2, 5000, [ref]$result) | Out-Null
Write-Host "$ClientDir in den PATH eingetragen."
Write-Host "VS Code und offene Terminals komplett neu starten, dann 'codexx' statt 'codex' eingeben."
+31
View File
@@ -0,0 +1,31 @@
# Tunnel-Client verwalten.
# tunnel.ps1 start Client im Hintergrund starten, Proxy fuer beliebige Programme
# tunnel.ps1 status laeuft der Client, und welche IP sieht das Internet?
# tunnel.ps1 stop Client beenden
# tunnel.ps1 run Client im Vordergrund mit ausfuehrlichem Log (Fehlersuche)
param([ValidateSet('start', 'status', 'stop', 'run')][string]$Action = 'status')
. "$PSScriptRoot\common.ps1"
switch ($Action) {
'start' {
Start-Tunnel
Write-Host "Tunnel laeuft. In den gewuenschten Programmen als HTTP-Proxy eintragen: $LocalProxy"
Write-Host "Beenden mit Tunnel-Stop.cmd."
}
'status' {
if (-not (Test-TunnelListening)) { Write-Host "Tunnel laeuft nicht."; break }
Write-Host "Tunnel lauscht auf $LocalProxy."
$ip = & curl.exe -s -m 15 -x $ProxyUrl https://api.ipify.org
if ($LASTEXITCODE -eq 0) { Write-Host "Oeffentliche IP ueber den Tunnel: $ip" }
else { Write-Host "Keine Verbindung durch den Tunnel. Log: $(Join-Path $LogDir 'wstunnel.log')" }
}
'stop' {
Stop-Tunnel
Write-Host "Tunnel beendet."
}
'run' {
Stop-Tunnel
Install-Wstunnel
Invoke-WithTunnelEnv { & $Exe @(Get-TunnelArgs 'INFO') }
}
}
+340
View File
@@ -0,0 +1,340 @@
# Installation
Schritt-für-Schritt-Einrichtung von Server (Raspberry Pi) und Client (Windows).
Alle Befehle sind zum Kopieren gedacht. Platzhalter in `GROSSBUCHSTABEN`
ersetzen.
Dauer: etwa 20 Minuten.
---
## Voraussetzungen
**Server**
- Raspberry Pi oder anderer Linux-Rechner mit Docker und Docker Compose
(`docker compose version` funktioniert)
- [Nginx Proxy Manager](https://nginxproxymanager.com/) läuft dort in Docker
- Eine Domain zeigt auf deinen Internetanschluss. Im Router ist Port 443 an den
Pi weitergeleitet.
- Im Nginx Proxy Manager gibt es für diese Domain einen Proxy Host mit gültigem
SSL-Zertifikat. Ein neues Zertifikat bekommst du im Proxy Host über den Tab
**SSL** → *Request a new SSL Certificate*.
- SSH-Zugang zum Pi mit einem Benutzer, der `sudo` darf
**Client**
- Windows 10 oder 11 mit Google Chrome
- Dieses Projekt als Ordner auf dem PC
---
## Teil 1 — Server
### 1.1 Dateien auf den Pi kopieren
In PowerShell im Projektordner (dort, wo `README.md` liegt):
```powershell
$Pi = 'BENUTZER@PI-ADRESSE'
ssh $Pi 'mkdir -p ~/wstunnel && chmod 700 ~/wstunnel'
scp server\docker-compose.yml server\restrictions.example.yaml server\firewall\tunnler-firewall.sh server\firewall\tunnler-firewall.service "${Pi}:wstunnel/"
```
Danach am Pi anmelden. Alle folgenden Befehle in Teil 1 laufen dort:
```powershell
ssh $Pi
```
### 1.2 Netz des Proxy Managers ermitteln
Der Tunnel-Container kommt in dasselbe Docker-Netz wie der Nginx Proxy Manager
und bekommt dort eine feste IP-Adresse.
```bash
cd ~/wstunnel
NPM_CONTAINER=$(docker ps --format '{{.Names}} {{.Image}}' | awk '/nginx-proxy-manager/ {print $1; exit}')
NPM_NETWORK=$(docker inspect "$NPM_CONTAINER" --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' | head -n 1)
SUBNET=$(docker network inspect "$NPM_NETWORK" --format '{{(index .IPAM.Config 0).Subnet}}')
TUNNEL_IP=$(python3 -c 'import ipaddress, sys; print(ipaddress.ip_network(sys.argv[1])[-6])' "$SUBNET")
printf 'NPM_NETWORK=%s\nTUNNEL_IP=%s\n' "$NPM_NETWORK" "$TUNNEL_IP" > .env
echo "Proxy Manager: $NPM_CONTAINER | Netz: $NPM_NETWORK ($SUBNET) | Tunnel-IP: $TUNNEL_IP"
```
Die letzte Zeile muss einen Container, ein Netz und eine IP nennen, zum
Beispiel:
```
Proxy Manager: nginx-app-1 | Netz: nginx_default (172.20.0.0/16) | Tunnel-IP: 172.20.255.250
```
Ist ein Feld leer oder heißt das Netz `bridge`, siehe
[Fehlerbehebung](#fehlerbehebung).
### 1.3 Secret erzeugen
Das Secret ist das Passwort des Tunnels. Es wird Teil des URL-Pfads.
```bash
cd ~/wstunnel
SECRET=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32)
sed "s/__SECRET__/$SECRET/" restrictions.example.yaml > restrictions.yaml
chmod 644 restrictions.yaml
echo "Secret: $SECRET"
```
Das Secret in einem Passwort-Manager speichern. Es wird in Schritt 1.6 und 2.2
gebraucht.
### 1.4 Tunnel-Server starten
```bash
cd ~/wstunnel
docker compose up -d
docker logs wstunnel --tail 5
```
Docker lädt das Image `ghcr.io/erebe/wstunnel` beim ersten Start selbst
herunter. Die Version steht in `docker-compose.yml`. In der Log-Ausgabe muss
folgende Zeile stehen:
```
Starting wstunnel server listening on 0.0.0.0:8090
```
### 1.5 Firewall einrichten
Die Regeln sperren für den Tunnel-Container alle privaten Netze. Über den Tunnel
ist damit weder das Heimnetz noch der Pi selbst erreichbar.
```bash
cd ~/wstunnel
. ./.env
echo "TUNNEL_IP=$TUNNEL_IP" | sudo tee /etc/default/tunnler-firewall
sudo install -m 755 tunnler-firewall.sh /usr/local/sbin/
sudo install -m 644 tunnler-firewall.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now tunnler-firewall
sudo iptables -S TUNNLER | head -n 3
```
Die Ausgabe beginnt mit `-N TUNNLER`. Die Regeln werden bei jedem Neustart
automatisch wieder gesetzt.
### 1.6 Nginx Proxy Manager konfigurieren
Die Werte für die Oberfläche anzeigen:
```bash
cd ~/wstunnel
. ./.env
SECRET=$(sed -n 's/.*PathPrefix "^\([A-Za-z0-9]*\)\$".*/\1/p' restrictions.yaml)
echo "Location: /$SECRET/"
echo "Forward: http $TUNNEL_IP 8090"
```
Im Nginx Proxy Manager **Hosts → Proxy Hosts** öffnen und beim Proxy Host der
Domain auf **Edit** klicken:
1. Tab **Details**: **Websockets Support** einschalten.
2. Tab **Custom Locations** → **Add location**:
| Feld | Wert |
|---|---|
| Location | `/SECRET/` aus der Ausgabe oben, mit beiden Schrägstrichen |
| Scheme | `http` |
| Forward Hostname / IP | Tunnel-IP aus der Ausgabe oben, ohne Schrägstrich |
| Forward Port | `8090` |
3. Beim Zahnrad der Location dieses Feld füllen:
```nginx
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
```
4. **Save**. Der Proxy Host muss danach als *Online* angezeigt werden.
Die bestehende Website unter `/` bleibt unverändert.
---
## Teil 2 — Client
### 2.1 Ordner ablegen
In PowerShell im Projektordner:
```powershell
New-Item -ItemType Directory -Force C:\Tools | Out-Null
Copy-Item -Recurse client C:\Tools\tunnler
```
Ein anderer Zielordner geht genauso. Admin-Rechte sind nicht nötig.
`wstunnel.exe` lädt der erste Start von GitHub nach `bin\` und prüft dabei die
SHA-256-Prüfsumme.
### 2.2 Konfiguration anlegen
```powershell
cd C:\Tools\tunnler
Copy-Item config.example.ps1 config.ps1
notepad config.ps1
```
In `config.ps1` zwei Werte eintragen und speichern:
```powershell
$TunnelSecret = 'SECRET'
$TunnelServer = 'wss://DOMAIN:443'
```
### 2.3 Server-Weg prüfen
```powershell
curl.exe -s -o NUL -w "%{http_code}`n" https://DOMAIN/SECRET/events
```
| Ergebnis | Bedeutung |
|---|---|
| `400` | Richtig: Die Anfrage erreicht den Tunnel-Server |
| `404` | Die Custom Location fehlt oder ist falsch geschrieben (1.6) |
| `502` | Der Container läuft nicht, oder die Forward-IP ist falsch (1.4, 1.6) |
### 2.4 Tunnel testen
`Chrome-Tunnel.cmd` doppelklicken. Es öffnet sich ein eigenes Chrome-Fenster.
Danach in PowerShell:
```powershell
cd C:\Tools\tunnler
.\Tunnel-Status.cmd
```
Die angezeigte IP muss die IP des Internetanschlusses zu Hause sein. Für einen
echten Test den PC mit einem Handy-Hotspot verbinden: Die IP darf dann nicht die
des Hotspots sein.
### 2.5 `codexx` einrichten
`PATH-eintragen.cmd` doppelklicken. Der Client-Ordner landet im Benutzer-PATH,
Admin-Rechte sind nicht nötig. Danach VS Code und alle Terminals komplett neu
starten. In jedem Projektordner startet `codexx` statt `codex` Codex über den
Tunnel.
Nach dem Verschieben des Client-Ordners den alten Eintrag unter
*Umgebungsvariablen für dieses Konto bearbeiten* → `Path` löschen und
`PATH-eintragen.cmd` erneut ausführen.
---
## Teil 3 — Einsatz hinter einem Firmen-Proxy
Netze mit erzwungenem Proxy lassen keine direkte Verbindung zu. Der Tunnel muss
dort durch den Proxy.
### 3.1 Proxy-Adresse herausfinden
```powershell
(Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings").AutoConfigURL
```
Die angezeigte URL im Browser öffnen. In der PAC-Datei nach einer Zeile wie
`PROXY proxy.firma.de:8080` suchen.
### 3.2 Proxy eintragen
In `config.ps1`:
```powershell
$UpstreamProxy = 'http://PROXY-HOST:PROXY-PORT'
```
Verlangt der Proxy eine Anmeldung, zusätzlich `$UpstreamProxyUser` und
`$UpstreamProxyPassword` setzen. Danach `Tunnel-Stop.cmd` ausführen und
`Chrome-Tunnel.cmd` neu starten.
### 3.3 Wenn der Proxy TLS aufbricht
In Edge `https://DOMAIN` öffnen, auf das Schloss klicken und das Zertifikat
ansehen. Ist der Aussteller nicht *Let's Encrypt*, bricht der Proxy die
Verschlüsselung auf. Der Tunnel scheitert dann an der Zertifikatsprüfung. Nur
in diesem Fall in `config.ps1` setzen:
```powershell
$SkipTlsVerify = $true
```
Der Proxy sieht dann das Secret. Nach dem Einsatz das Secret tauschen.
---
## Secret tauschen
Auf dem Pi:
```bash
cd ~/wstunnel
OLD=$(sed -n 's/.*PathPrefix "^\([A-Za-z0-9]*\)\$".*/\1/p' restrictions.yaml)
NEW=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32)
sed -i "s/$OLD/$NEW/" restrictions.yaml
echo "Neues Secret: $NEW"
```
Der Server lädt die Datei automatisch neu. Danach:
1. Im Nginx Proxy Manager die Custom Location auf `/NEUES-SECRET/` ändern und
speichern.
2. Auf jedem PC in `config.ps1` `$TunnelSecret` ersetzen, dann `Tunnel-Stop.cmd`
ausführen.
---
## Aktualisieren
In `~/wstunnel/docker-compose.yml` die Image-Version erhöhen
([Releases](https://github.com/erebe/wstunnel/releases)), dann:
```bash
cd ~/wstunnel
docker compose pull
docker compose up -d
```
Den Client auf dieselbe Version bringen: In `client\scripts\common.ps1`
`$WstunnelVersion` anpassen, `Tunnel-Stop.cmd` ausführen und
`client\bin\wstunnel.exe` löschen. Der nächste Start lädt die neue Version.
---
## Deinstallieren
1. Im Nginx Proxy Manager die Custom Location entfernen.
2. Auf dem Pi:
```bash
cd ~/wstunnel && docker compose down
sudo systemctl disable --now tunnler-firewall
sudo rm /etc/systemd/system/tunnler-firewall.service /usr/local/sbin/tunnler-firewall.sh /etc/default/tunnler-firewall
sudo systemctl daemon-reload
rm -r ~/wstunnel
```
3. Auf dem PC den Client-Ordner löschen. Es wurde nichts installiert.
---
## Fehlerbehebung
| Symptom | Lösung |
|---|---|
| 1.2: `Proxy Manager:` ist leer | Der Container heißt anders. `docker ps` ausführen und den Namen von Hand setzen: `NPM_CONTAINER=NAME`, dann ab der zweiten Zeile wiederholen. |
| 1.2: Netz heißt `bridge` | Im Standardnetz sind keine festen IPs möglich. Den Proxy Manager in ein eigenes Netz verschieben, zum Beispiel per Docker Compose. |
| 1.4: `Address already in use` / `invalid address` | Die Tunnel-IP ist belegt oder liegt außerhalb des Subnetzes. In `~/wstunnel/.env` eine andere freie IP aus dem Subnetz eintragen und 1.4 bis 1.6 wiederholen. |
| 1.6: Proxy Host *Offline* | Die Location in NPM löschen und Schreibweise prüfen. Im Feld *Forward Hostname* darf kein Pfad stehen. |
| Client: `Tunnel-Client lauscht nicht` | `logs\wstunnel.err.log` im Client-Ordner lesen. Ein anderes Programm nutzt eventuell Port 1080; dann `$LocalProxy` in `config.ps1` ändern. |
| Client verbindet nicht, Timeout | Im Firmennetz fehlt `$UpstreamProxy` (3.2). Sonst Portweiterleitung und DNS der Domain prüfen. |
| Seite lädt nicht, Client-Log zeigt `400` | Das Ziel ist gesperrt (nur Ports 80/443, keine privaten Adressen). Den Grund zeigt `docker logs wstunnel` auf dem Pi in der Zeile `Rejecting …`. |
| Codex nutzt den Tunnel nicht | Codex mit `codexx` starten, nicht direkt mit `codex`. Fehlt der Befehl, `PATH-eintragen.cmd` ausführen (2.5). |
+33
View File
@@ -0,0 +1,33 @@
services:
wstunnel:
image: ghcr.io/erebe/wstunnel:v11.1.0
container_name: wstunnel
restart: unless-stopped
networks:
npm: # nur fuer den Proxy Manager erreichbar, kein ports:
ipv4_address: ${TUNNEL_IP:-172.20.250.250} # feste IP: NPM haengt nicht von Docker-DNS ab
command:
- /home/app/wstunnel # Image startet ueber dumb-init, Binaerpfad noetig
- server
- --restrict-config=/home/app/restrictions.yaml
- --dns-resolver=dns://1.1.1.1 # oeffentliches DNS: lokale Namen sind nicht aufloesbar
- --dns-resolver=dns://9.9.9.9
- --dns-resolver-prefer-ipv4
- ws://0.0.0.0:8090 # NPM terminiert TLS, intern reicht ws
volumes:
- ./restrictions.yaml:/home/app/restrictions.yaml:ro
read_only: true
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
mem_limit: 64m
logging:
driver: json-file
options:
max-size: 1m
max-file: "3"
# Werte aus .env (siehe docs/installation.md), Standard = bestehende Installation
networks:
npm:
external: true
name: ${NPM_NETWORK:-nginx_default}
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=tunnler: wstunnel-Container vom Heimnetz trennen
After=docker.service
PartOf=docker.service
[Service]
Type=oneshot
EnvironmentFile=-/etc/default/tunnler-firewall
RemainAfterExit=yes
ExecStart=/usr/local/sbin/tunnler-firewall.sh
ExecStop=/usr/local/sbin/tunnler-firewall.sh --remove
[Install]
WantedBy=multi-user.target docker.service
+27
View File
@@ -0,0 +1,27 @@
#!/bin/sh
# Trennt den wstunnel-Container (feste IP, TUNNEL_IP aus /etc/default/tunnler-firewall) vom Heimnetz
# und vom Pi selbst. Hostnamen, die per oeffentlichem DNS auf private IPs zeigen
# (z. B. 192-168-0-1.nip.io), kann wstunnel selbst nicht abfangen.
# tunnler-firewall.sh Regeln setzen (idempotent)
# tunnler-firewall.sh --remove Regeln entfernen
set -e
SRC=${TUNNEL_IP:-172.20.250.250}
for chain in DOCKER-USER INPUT; do
while iptables -D "$chain" -s "$SRC" -j TUNNLER 2>/dev/null; do :; done
done
if [ "$1" = "--remove" ]; then
iptables -F TUNNLER 2>/dev/null || true
iptables -X TUNNLER 2>/dev/null || true
exit 0
fi
iptables -N TUNNLER 2>/dev/null || iptables -F TUNNLER
iptables -A TUNNLER -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
for net in 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16; do
iptables -A TUNNLER -d "$net" -j DROP
done
iptables -N DOCKER-USER 2>/dev/null || true
iptables -I DOCKER-USER -s "$SRC" -j TUNNLER
iptables -I INPUT -s "$SRC" -j TUNNLER
+99
View File
@@ -0,0 +1,99 @@
# wstunnel-Server: Zugriffsregeln (wird bei Aenderung automatisch neu geladen).
# Vorlage: Platzhalter im PathPrefix durch das Secret ersetzen, als restrictions.yaml speichern.
#
# Wirkung:
# - Nur Clients mit dem geheimen Pfad-Praefix werden akzeptiert.
# - Nur Forward-Tunnel (keine Reverse-Tunnel), nur TCP, nur Ziel-Port 80/443.
# - Hostnamen: mindestens ein Punkt, TLD aus Buchstaben, nie *.localhost.
# Lokale Namen (pi.hole, *.home, Container-Namen) loest der Server nicht auf,
# weil er per --dns-resolver nur oeffentliches DNS fragt (siehe docker-compose.yml).
# - IP-Adressen als Ziel: nur oeffentliche IPv4-Bereiche (privat, Loopback,
# Link-Local, CGNAT, Multicast ausgeschlossen) und globales IPv6.
restrictions:
- name: "tunnler"
description: "Forward-Tunnel mit Secret, TCP 80/443, keine privaten Ziele"
match:
- !PathPrefix "^__SECRET__$"
allow:
- !Tunnel
protocol:
- Tcp
port:
- 80
- 443
host: '(?i)^([a-z0-9_-]+\.)+([a-z]{2,8}|[a-z]{10,63}|[a-km-z][a-z]{8}|l[a-np-z][a-z]{7}|lo[a-bd-z][a-z]{6}|loc[b-z][a-z]{5}|loca[a-km-z][a-z]{4}|local[a-gi-z][a-z]{3}|localh[a-np-z][a-z]{2}|localho[a-rt-z][a-z]|localhos[a-su-z]|xn--[a-z0-9-]+)$'
cidr:
- 1.0.0.0/8
- 2.0.0.0/7
- 4.0.0.0/6
- 8.0.0.0/7
- 11.0.0.0/8
- 12.0.0.0/6
- 16.0.0.0/4
- 32.0.0.0/3
- 64.0.0.0/3
- 96.0.0.0/6
- 100.0.0.0/10
- 100.128.0.0/9
- 101.0.0.0/8
- 102.0.0.0/7
- 104.0.0.0/5
- 112.0.0.0/5
- 120.0.0.0/6
- 124.0.0.0/7
- 126.0.0.0/8
- 128.0.0.0/3
- 160.0.0.0/5
- 168.0.0.0/8
- 169.0.0.0/9
- 169.128.0.0/10
- 169.192.0.0/11
- 169.224.0.0/12
- 169.240.0.0/13
- 169.248.0.0/14
- 169.252.0.0/15
- 169.255.0.0/16
- 170.0.0.0/7
- 172.0.0.0/12
- 172.32.0.0/11
- 172.64.0.0/10
- 172.128.0.0/9
- 173.0.0.0/8
- 174.0.0.0/7
- 176.0.0.0/4
- 192.0.1.0/24
- 192.0.2.0/23
- 192.0.4.0/22
- 192.0.8.0/21
- 192.0.16.0/20
- 192.0.32.0/19
- 192.0.64.0/18
- 192.0.128.0/17
- 192.1.0.0/16
- 192.2.0.0/15
- 192.4.0.0/14
- 192.8.0.0/13
- 192.16.0.0/12
- 192.32.0.0/11
- 192.64.0.0/10
- 192.128.0.0/11
- 192.160.0.0/13
- 192.169.0.0/16
- 192.170.0.0/15
- 192.172.0.0/14
- 192.176.0.0/12
- 192.192.0.0/10
- 193.0.0.0/8
- 194.0.0.0/7
- 196.0.0.0/7
- 198.0.0.0/12
- 198.16.0.0/15
- 198.20.0.0/14
- 198.24.0.0/13
- 198.32.0.0/11
- 198.64.0.0/10
- 198.128.0.0/9
- 199.0.0.0/8
- 200.0.0.0/5
- 208.0.0.0/4
- 2000::/3