AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
A self-hosted GOST tunnel manager. Expose anything local to the public internet.
Translation pending
Developer toolsGo · JavaScriptGitHub @go-gostOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
GOST Tunnel Manager
Translation pending
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
GOST tunnel manager — a self-hosted web UI and API for creating and managing reverse proxy tunnels through the GOST network.
Expose local services to the public internet without opening firewall ports. Wisper handles the tunnel lifecycle — create, start, stop, update, and monitor — through a browser-based UI.Read the full README ↗GOST tunnel manager — a self-hosted web UI and API for creating and managing reverse proxy tunnels through the GOST network.
Silent in · Silent out. / 静入 · 静出
Expose local services to the public internet without opening firewall ports. Wisper handles the tunnel lifecycle — create, start, stop, update, and monitor — through a browser-based UI.
# Build and run
make web
go build -o wisper .
./wisper
# Open http://localhost:8900 in your browser
Or with a custom port:
./wisper -addr :9000
Print version:
./wisper -version
| Type | Description | Default Entrypoint |
|---|---|---|
| File | Serve a local directory over HTTPS | https://<hash>.gost.run |
| HTTP | Expose a local HTTP service | https://<hash>.gost.run |
| TCP | Relay a raw TCP connection | tcp://<hash>.gost.run:<port> |
| UDP | Relay a raw UDP connection | udp://<hash>.gost.run:<port> |
| P2P | Expose a local service to peers by public key (private p2p mode) | Dialed by peer key over the DERP relay, no public URL |
| Type | Description |
|---|---|
| TCP | Listen locally and forward into a GOST tunnel |
| UDP | Same as TCP entrypoint, with keepalive + TTL support |
| P2P | Forward local traffic to a peer by its public key, over an inner tcp (default) or udp protocol |
| TUN | Join a peer's virtual network through a tun device (a tun peer; needs admin rights) |
P2P tunnels and entrypoints require a DERP relay configured in Settings. A p2p tunnel routes only the peers on its allowlist (the key is the credential); a tun entrypoint needs administrator rights to create the tun device.
Config file: ~/.config/wisper/wisper.yaml (YAML)
Logs: ~/.config/wisper/logs/wisper.log (JSON format, 10 MB rotation, 7-day retention)
For containers and manual debugging, -log.output and -log.level — or the
WISPER_LOG_OUTPUT / WISPER_LOG_LEVEL env vars — override the config file for
that run only (nothing is written back to it). Precedence: flag > env > config.
wisper -log.output stderr -log.level debug
WISPER_LOG_LEVEL=debug wisper
The Docker image sets WISPER_LOG_OUTPUT=stderr, so docker logs shows output;
docker run -e WISPER_LOG_OUTPUT=<path> (or -log.output <path>) sends it back
to a file.
Private p2p tunnels, entrypoints and tun peers share one deployment-level p2p host. Set its relay in Settings → P2P, or in the config file:
p2p:
derp: wss://relay.example.com/derp # DERP relay URL (required for p2p)
secure: true # verify the relay's TLS certificate (default: true)
caFile: /path/to/ca.pem # trust a self-signed relay
stun: relay.example.com:3478 # STUN for the IPv4 direct path (default: derived from the relay host)
direct: true # try a hole-punched path first, fall back to the relay (default: true)
The p2p host's public key (shown on the P2P settings page) is this instance's identity; peers dial it directly and no public URL is assigned.
Traffic inspection is served by an external inspector instance. Set its base URL in Settings → Inspector; a Traffic Inspection entry then appears on tunnel detail pages. Empty disables the feature.
All routes under /api/:
| Method | Route | Description |
|---|---|---|
GET | /api/tunnels | List all tunnels |
POST | /api/tunnels | Create tunnel |
GET | /api/tunnels/{id} | Get one tunnel |
PUT | /api/tunnels/{id} | Update tunnel (replaces + restarts) |
DELETE | /api/tunnels/{id} | Delete tunnel |
POST | /api/tunnels/{id}/start | Start a stopped tunnel |
POST | /api/tunnels/{id}/stop | Stop a running tunnel |
PUT | /api/tunnels/{id}/peers | Update a p2p tunnel's peer allowlist + aliases |
POST | /api/tunnels/{id}/stats/reset | Reset a tunnel's stats baseline |
GET | /api/entrypoints | List all entrypoints |
POST | /api/entrypoints | Create entrypoint |
GET | /api/entrypoints/{id} | Get one entrypoint |
PUT | /api/entrypoints/{id} | Update entrypoint |
DELETE | /api/entrypoints/{id} | Delete entrypoint |
POST | /api/entrypoints/{id}/start | Start a stopped entrypoint |
POST | /api/entrypoints/{id}/stop | Stop a running entrypoint |
POST | /api/entrypoints/{id}/stats/reset | Reset an entrypoint's stats baseline |
GET | /api/stats | Aggregated stats for all tunnels + entrypoints |
Wisper-IdA client may send a short opaque id on any request:
Wisper-Id: 3f9a1c2b
The backend logs it on the mutation line (id=…, at debug level) and the p2p
seam logs it (action=…) on the calls that action started — the host's
Listen, and each peer's Warm/Punch — so a UI click can be joined with the
p2p work it set in motion. The web UI sends a fresh id on every request.
An absent header gets a generated id (8 hex chars), so a curl, the CLI, or an
older bundle is still correlatable. The value is a label only: it is never
authentication, never parsed, and never able to change behaviour.
Correlation covers the calls an action initiates. p2p's background logs (punch rounds, session death, keepalive) stay action-free: one punch serves every caller and a session outlives any click, so tagging them would be a fabricated join.
# Run from Docker Hub
docker run -p 8900:8900 -v ~/.config/wisper:/root/.config/wisper gogost/wisper:latest
# Go binaries for Linux, macOS, Windows
make all
# Output: dist/linux-amd64/wisper, dist/darwin-arm64/wisper, etc.
make web # conditional rebuild (stamp-based — skips if no changes)
make web-force # force rebuild
make typecheck # type-check TypeScript (tsc --noEmit)
make ui-test # Playwright UI e2e against a real wisper binary
# Linux .deb + .AppImage (Docker cross-compile)
make linux-installer
# Windows NSIS installer (Docker cross-compile)
make windows-installer
# Development mode (hot-reload frontend + sidecar)
make tauri-dev
# Debug APK (pulls the prebuilt toolchain image; no host Node/Go/Android SDK needed)
make android # → dist/android/app-debug.apk
# Build the toolchain image locally when the registry is unreachable
make android-image # ANDROID_IMAGE / ANDROID_OUT override image and output dir
# Release APK (signed, needs keystore secrets)
make android-release
make android runs one multi-stage docker build: a node:22 stage builds the
web UI, the toolchain image builds libwisper.so via the NDK and the APK via
Gradle, and a scratch stage hands back just the APK.
A revoked VPN — another app replaced it, or the user or the system disconnected it — stops the tun entrypoint that held the device instead of racing it back; wisper does not try to tell the two apart (the probe for "is another app's VPN up?" answered yes on a manual disconnect, seeing its own network still tearing down). Starting the entrypoint again is one tap, and that is what preempts the other app.
┌─────────────────────────────────────────────┐
│ Browser │
│ (Lit web UI on localhost) │
└──────────────────┬──────────────────────────┘
│ HTTP /api/*
┌──────────────────▼──────────────────────────┐
│ Go HTTP Server │
│ ┌─────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ API │ │ Runner │ │ Web (SPA) │ │
│ │ handlers│ │ (stats) │ │ //go:embed │ │
│ └────┬────┘ └──────────┘ └─────────────┘ │
│ │ │
│ ┌────▼────────────────────────────────────┐ │
│ │ Tunnel Manager │ │
│ │ File │ HTTP │ TCP │ UDP │ P2P │ Entrypts│ │
│ └────┬────────────────────────────────────┘ │
└───────┼──────────────────────────────────────┘
│ WSS / GOST relay
┌───────▼──────────┐
│ GOST Network │
│(wisper.gost.run) │
└──────────────────┘
Wisper is a single Go module (github.com/go-gost/wisper). The Lit web app (web-src/) compiles to web/ via Vite and is embedded into the Go binary via //go:embed.
| Package | Purpose |
|---|---|
main | Entry point, flag parsing, server lifecycle |
web.go | Embeds web build, serves SPA |
config/ | Settings + tunnel persistence (atomic, thread-safe) |
tunnel/ | Tunnel interface + 5 tunnel types (incl. p2p) + chain builder |
tunnel/entrypoint/ | TCP/UDP/P2P/TUN entrypoint types |
event/ | Bounded per-object + host event history, persisted with the config |
api/ | REST handlers (Go 1.22+ ServeMux with method routing) |
runner/ | Background task scheduler with cancel-by-ID |
runner/task/ | Stats polling task |
version/ | Version string (set via ldflags) |
MIT
GET | /api/events | Host-level event history |
DELETE | /api/events | Clear the host-level event history |
GET | /api/p2p | This host's p2p identity (public key, status) |
POST | /api/p2p/reset | Rotate the p2p identity; requires {"confirm":true} and restarts p2p consumers |
POST | /api/p2p/test | Probe the configured DERP relay |
POST | /api/p2p/test-stun | Probe the STUN server |
GET | /api/p2p/pending | Peers that knocked but are not allowlisted |
DELETE | /api/p2p/pending/{key} | Dismiss a pending peer |
GET | /api/p2p/doctor | The p2p diagnostic report as plain text (?peer= narrows it to one peer) |
GET | /api/version | Version info |
GET | /api/config | Get app settings |
PUT | /api/config | Update app settings |