Give every new connection a German Tor horizon
July 16, 2026 · View on GitHub
Véspero
Una luz. Todos los rumbos.
One light. Every direction.
A self-hosted, pluggable egress controller behind one quiet, stable proxy endpoint.
The connection remains. Only the horizon moves.
Véspero sits between the proxy you already trust and the network beyond it. Your client keeps one HTTP/SOCKS5 address; the web console or API decides where new TCP connections leave—through the host, a VPNGate tunnel, or a country-constrained Tor circuit.
It is an egress control plane, not another client protocol. It does not claim anonymity, and it does not reimplement Tor or OpenVPN.
One relay. Three kinds of horizon. The active provider and observed exit IP remain visible.
One address, many horizons
| What stays still | What can move |
|---|---|
| Client configuration | Exit provider |
127.0.0.1:19080 relay | Direct, VPNGate, or Tor |
| HTTP/SOCKS5 contract | Country, node, and routing policy |
| Established connections | New connections only |
| Automation surface | The route behind it |
A switch is deliberately calm: existing connections finish on the socket they already own; connections accepted after the switch use the new route. If a selected provider cannot become ready, Véspero fails closed instead of silently leaking new traffic through Direct.
What is already real
- One stable HTTP and SOCKS5 TCP relay with optional user/password authentication and a connection limit.
- Runtime switching between Direct, VPNGate, and a managed client-only Tor process.
- A polished web console for provider identity, country selection, exit-IP checks, route health, filters, policy, and event history.
- A typed OpenAPI 3.1 control surface with embedded Swagger UI for people, scripts, and agents.
- VPNGate catalog refresh, parallel probes, favorites, country/IP-type policy, blacklist TTL, watchdog failover, and tunnel-bound DNS.
- Debian packages, systemd integration, Windows bundles, and compatibility with existing
VGL_*deployments.
The route in one glance
flowchart LR
Client["Unchanged client"] --> Front["sing-box · Mihomo · Hysteria 2<br/>browser · agent"]
Front -->|"HTTP / SOCKS5"| Relay["Véspero relay<br/>127.0.0.1:19080"]
Console["Web console"] --> Control["Egress controller"]
API["OpenAPI 3.1"] --> Control
Control -. "atomic route selection" .-> Relay
Relay --> Choice{"Route for a<br/>new connection"}
Choice --> Direct["Direct<br/>host network"]
Choice --> VPN["VPNGate<br/>bound OpenVPN interface"]
Choice --> Tor["Tor<br/>local SOCKS upstream"]
classDef core fill:#082f36,stroke:#45d5c1,color:#f6fffd,stroke-width:2px;
classDef control fill:#e9faf6,stroke:#159b8c,color:#082f36;
classDef provider fill:#fffaf0,stroke:#ff8e72,color:#34251e;
class Relay core;
class Console,API,Control control;
class Direct,VPN,Tor provider;
The data path and control path stay separate. Front ends such as Hysteria 2, sing-box, or Mihomo remain responsible for client-facing transport; Véspero owns the egress choice behind their local upstream.
Providers
| Provider | Identity in the UI | How it is selected | Runtime path |
|---|---|---|---|
| Direct | LOCAL | One click | Host network |
| VPNGate | COMMUNITY | Node, country, favorites, or automatic policy | Managed OpenVPN process and interface-bound sockets |
| Tor | NETWORK · TCP | Automatic or ISO country code | Managed client-only Tor process and SOCKS5 chaining |
All providers converge on a small internal model—Direct, Interface, or SOCKS5 upstream. That is the extension seam for the next provider; clients and the stable relay do not have to learn anything new.
Live route inventory, visible provider provenance, deliberate policy, and a history of every transition.
Quick start
Linux is the complete target because isolated VPNGate routing depends on SO_BINDTODEVICE and policy routing. Direct and Tor also work on Windows.
Build prerequisites: Rust, Node.js 22+, OpenVPN, and Tor.
git clone https://github.com/tianrking/Vespero.git
cd Vespero
sudo bash scripts/install.sh
The installer builds the console and controller, installs vespero.service, and generates a control password in the root-only /etc/default/vespero file.
Keep the control plane local and open it through SSH:
ssh -L 18081:127.0.0.1:18081 root@YOUR_VPS
| Surface | Default |
|---|---|
| Web console | http://127.0.0.1:18081 |
| Swagger UI | http://127.0.0.1:18081/docs |
| OpenAPI document | http://127.0.0.1:18081/openapi.json |
| Stable relay | socks5://127.0.0.1:19080 or http://127.0.0.1:19080 |
| Service | vespero.service |
| Configuration | /etc/default/vespero |
Never expose an unauthenticated relay to the Internet. For a public control surface, keep Véspero on loopback and terminate HTTPS with the reverse proxy you already operate.
Put Véspero behind your ingress
Your users keep connecting to the transport they already have. Only its server-side upstream points to Véspero.
Hysteria 2 server
outbounds:
- name: vespero
type: socks5
socks5:
addr: 127.0.0.1:19080
# Véspero's relay is TCP-only today. Hysteria still uses QUIC/UDP as transport.
disableUDP: true
sing-box outbound
{
"type": "socks",
"tag": "vespero",
"server": "127.0.0.1",
"server_port": 19080
}
Mihomo proxy
proxies:
- name: vespero
type: socks5
server: 127.0.0.1
port: 19080
Control it from the web—or from code
Protected endpoints receive the control password as Authorization: Bearer <password>. VESPERO_TOKEN remains the internal environment-variable name for compatibility; the person using the console simply sees a password.
PASSWORD="$(sudo sed -n 's/^VESPERO_TOKEN=//p' /etc/default/vespero)"
# Discover installed providers and their capabilities
curl -H "Authorization: Bearer $PASSWORD" \
http://127.0.0.1:18081/api/providers
# Give every new connection a German Tor horizon
curl -X POST \
-H "Authorization: Bearer $PASSWORD" \
-H "Content-Type: application/json" \
-d '{"provider":"tor","country":"DE"}' \
http://127.0.0.1:18081/api/egress/select
The embedded Swagger UI can authorize, inspect schemas, and execute every control endpoint without installing another tool.
The same control plane is legible to a person, a script, or an agent.
Configuration
The important defaults are intentionally private:
| Variable | Default | Purpose |
|---|---|---|
VESPERO_CONTROL | 127.0.0.1:18081 | Web and API listener |
VESPERO_RELAY | 127.0.0.1:19080 | Stable HTTP/SOCKS5 listener |
VESPERO_TOKEN | Generated on install | Control password; compatibility name |
VESPERO_TUN | vespero0 | VPNGate OpenVPN interface |
VESPERO_TOR_CMD | tor | Tor executable |
VESPERO_TOR_SOCKS | 127.0.0.1:19050 | Managed Tor SOCKS port |
VESPERO_RELAY_USER / VESPERO_RELAY_PASS | Empty | Optional relay authentication |
See packaging/default.env for the complete set. VESPERO_* values take precedence; legacy VGL_* names remain readable during migration.
Honest boundaries
- The relay currently carries TCP. UDP-capable ingress protocols must disable proxied UDP or handle it outside Véspero.
- Tor country selection is a circuit constraint, not a permanent or dedicated IP, and some countries may have no usable exits.
- VPNGate is a live volunteer network. Availability, ownership, performance, and trust change over time.
- Véspero exposes provider identity precisely because an exit is a route—not a promise of anonymity.
- Unsafe application behavior remains unsafe regardless of which provider is selected.
Build confidence, not just binaries
python3 scripts/check-readmes.py
cargo fmt -- --check
cargo clippy --all-targets --locked -- -D warnings
cargo test --locked
cd web
npm ci
npm run build
CI repeats the work across Ubuntu and Windows, builds Debian and Windows packages, installs them in clean target environments, verifies authentication and the embedded OpenAPI console, and publishes artifacts only after the matrix is green.
The Linux live-provider test can also exercise Direct, Tor, country-constrained Tor, VPNGate, and restoration through the real relay:
sudo VESPERO_E2E_FULL=1 ./scripts/e2e-linux.sh
An installed Linux gateway has a stricter, disruptive release acceptance test. It verifies live preemption, cancellation, process/TUN cleanup, both relay protocols, and—when configured—the complete Hysteria2 path. It always restores Direct on exit:
sudo VESPERO_REQUIRE_HYSTERIA=on ./scripts/live-ubuntu-check.sh
Architecture details and provider extension rules live in docs/ARCHITECTURE.md; the next deliberate steps live in docs/ROADMAP.md.
It changes the sky beyond the connection.