whitebox for Prometheus provides availability monitoring of external VPN services powered by VMESS, VLESS, TROJAN, WG, AWG, SS and HYSTERIA.
The features that distinguish whitebox:
- Multi-protocol VPN Probing: Supports probing of external VPN services including VMESS, VLESS, Trojan, Wireguard, AmneziaWG, Shadowsocks and Hysteria2.
- RESTful API Service: Exposes HTTP endpoints for on-demand or scheduled connectivity checks.
- Custom Probe Configuration: Accepts probe parameters such as connection details, target URLs, response validation rules, and configurable timeouts.
- Prometheus Metrics Integration: Exposes key probe results as Prometheus metrics.
whitebox ships a ready-to-run image on Docker Hub: rxyvea/whitebox.
docker run --rm -d --name whitebox -p 9116:9116 rxyvea/whitebox:latestThat is a complete deployment. Whitebox starts with a built-in default scope and listens on :9116.
Probe any target through a VPN connection URI. ctx is the URL-encoded connection URI, target is what to fetch through the resulting tunnel:
curl -sG http://localhost:9116/probe \
--data-urlencode 'ctx=vless://c9f5228c-8870-47bd-a92f-9b38c7c02b08@1.2.3.4:443?type=tcp&encryption=none&security=reality&pbk=DF-3KL2W4RuNB2HgsEDmLqHLvvTTN4_QfwUCUn8Uhy0&fp=firefox&sni=ce-cdn.icloud-content.com&sid=620352b7&spx=%2F&flow=xtls-rprx-vision' \
--data-urlencode 'target=https://google.com'A Prometheus-style metrics page describing that single probe comes back. Refer for Reading a probe response below.
The endpoints:
| Endpoint | Purpose |
|---|---|
/probe?ctx=…&target=…&scope=… |
Probe one tunnel. scope is optional and defaults to default. |
/metrics |
Exporter's own process, Go runtime and xray cache metrics. |
Custom scopes, timeouts, request bodies and fail_if validation rules live in a YAML file mounted into the container. Grab the annotated example and mount it:
curl -O https://raw.githubusercontent.com/quyxishi/whitebox/main/whitebox.yml
docker run --rm -d --name whitebox -p 9116:9116 \
-v "$PWD/whitebox.yml:/etc/whitebox/whitebox.yml:ro" \
rxyvea/whitebox:latest --config.file=/etc/whitebox/whitebox.ymlSelect a scope per probe with &scope=<scope_name>. See Whitebox Configuration for what the file can express.
Tip
Config is reloaded in place on SIGHUP: docker kill -s HUP whitebox.
Drop this into a docker-compose.yml next to your whitebox.yml:
services:
whitebox:
image: rxyvea/whitebox:latest
container_name: whitebox
restart: unless-stopped
ports:
- ${WHITEBOX_PORT:-9116}:9116
command:
- '--config.file=/etc/whitebox/whitebox.yml'
- '--log.level=info'
volumes:
- ./whitebox.yml:/etc/whitebox/whitebox.ymlexamples/ holds a working whitebox + blackbox + Prometheus + Grafana deployment, already wired together.
git clone -b main https://github.com/quyxishi/whitebox
cd whitebox/examples
docker compose up -dGrafana lands on http://localhost:3000 (admin/admin by default), Prometheus on http://localhost:9090 — check its /targets page for the whitebox job. Point whitebox-sd-config.yml at your own connection URIs to get real data, then import hemera-dashboard.json into Grafana to visualise it.
Requires Go 1.26+, or just Docker for the image build:
git clone -b main https://github.com/quyxishi/whitebox
cd whitebox
docker build --tag whitebox .
docker run --rm -d -p 9116:9116 whiteboxSee Development for the local (non-Docker) loop.
Verbosity is controlled by the --log.level flag (-l) or the WHITEBOX_LOG_LEVEL environment variable, one of debug, info (default), warn, error:
docker run --rm -d -p 9116:9116 -e WHITEBOX_LOG_LEVEL=warn rxyvea/whitebox:latestOn debug, per-request access logs and verbose xray-core tunnel logs are emitted as well; on any higher level both are suppressed.
By default whitebox binds to :9116. Use the --web.listen-address flag or the WHITEBOX_LISTEN_ADDRESS environment variable to change the port (or the interface), accepting either [host]:port or a bare port:
sudo docker run --rm -d -p 9200:9200 -e WHITEBOX_LISTEN_ADDRESS=:9200 whiteboxWith docker-compose.yaml the published host port can be overridden through the WHITEBOX_PORT variable, e.g. WHITEBOX_PORT=9200 sudo docker compose up -d.
Every probe returns the outcome as Prometheus metrics:
# HELP tun_probe_duration_seconds Returns how long the probe took to complete in seconds
# TYPE tun_probe_duration_seconds gauge
tun_probe_duration_seconds 1.554994
# HELP tun_probe_http_content_length_bytes Length of HTTP content response in bytes
# TYPE tun_probe_http_content_length_bytes gauge
tun_probe_http_content_length_bytes -1
# HELP tun_probe_http_duration_seconds Duration of HTTP request by phase, summed over all traces
# TYPE tun_probe_http_duration_seconds gauge
tun_probe_http_duration_seconds{phase="connect"} 0.1998826
tun_probe_http_duration_seconds{phase="processing"} 0.3262475
tun_probe_http_duration_seconds{phase="resolve"} 0
tun_probe_http_duration_seconds{phase="tls"} 1.2223279
tun_probe_http_duration_seconds{phase="transfer"} 0.0064185
# HELP tun_probe_http_redirects The number of redirects
# TYPE tun_probe_http_redirects gauge
tun_probe_http_redirects 1
# HELP tun_probe_http_ssl Indicates if SSL was used for the final trace
# TYPE tun_probe_http_ssl gauge
tun_probe_http_ssl 1
# HELP tun_probe_http_status_code Response HTTP status code
# TYPE tun_probe_http_status_code gauge
tun_probe_http_status_code 200
# HELP tun_probe_http_uncompressed_body_length_bytes Length of uncompressed response body in bytes
# TYPE tun_probe_http_uncompressed_body_length_bytes gauge
tun_probe_http_uncompressed_body_length_bytes 17650
# HELP tun_probe_instance_cached Indicates if the probe reused an already-built xray instance
# TYPE tun_probe_instance_cached gauge
tun_probe_instance_cached 1
# HELP tun_probe_success Displays whether or not the probe over tunnel was a success
# TYPE tun_probe_success gauge
tun_probe_success 1/probe describes a single tunnel. The exporter process itself is exposed separately:
http://localhost:9116/metricsThis serves the Go runtime and process collectors (go_memstats_heap_inuse_bytes, go_goroutines, ...) alongside xray instance cache statistics:
| Metric | Description |
|---|---|
whitebox_xray_instances |
Instances currently held by the cache |
whitebox_xray_instances_in_use |
Instances currently leased by an in-flight probe |
whitebox_xray_cache_hits_total / _misses_total |
Probes that reused vs. built an instance |
whitebox_xray_cache_evictions_total{reason} |
Evictions by ttl, lru, reload or shutdown |
whitebox_xray_cache_overflow_total |
Times max_entries was exceeded because every instance was in use |
whitebox_xray_instance_build_duration_seconds |
Cost of constructing and starting an instance |
whitebox_xray_instance_build_failures_total |
Failures to construct or start an instance |
In steady state whitebox_xray_instances should settle at the number of distinct connection URIs scraped, and whitebox_xray_cache_misses_total should stop increasing.
Refer to the annotated example configuration and code reference for implementation details.
Whitebox reuses started xray instances across probes, keyed by the generated xray config, rather than building one per request.
This is required for bounded memory, not merely an optimisation: xray-core's xhttp, grpc and hysteria dialers cache per-connection state in package-level maps keyed by the *internet.MemoryStreamConfig pointer that core.New allocates, and never delete from them. One instance per probe therefore leaks one permanent entry per probe, which is issue #10.
Consequences worth knowing:
- For
xhttp, whitebox injectsxmux.hMaxRequestTimes: 1into generated configs so each probe still establishes a fresh transport connection to the VPN server. An explicitxmuxin the connection URI is always left untouched. - For
grpc,hysteria2andwireguardthere is no equivalent knob, so consecutive probes may share the underlying connection. A dead server still fails the probe; what is not re-exercised every probe is the handshake itself. Setinstance_cache.enabled: falseif per-probe handshake coverage on those transports matters more than flat memory. raw/tcp,ws,httpupgradeandkcpare unaffected — they have no dialer-level cache and reconnect per probe regardless.
Use tun_probe_instance_cached to tell cold probes from warm ones when reading tun_probe_http_duration_seconds.
Whitebox follows the multi-target exporter pattern.
scrape_configs:
- job_name: 'whitebox'
metrics_path: /probe
file_sd_configs:
- files: [ '/etc/prometheus/whitebox-sd-config.yml' ] # File service discovery configurations (targets).
relabel_configs:
- source_labels: [__address__]
target_label: __param_target # 'target' -> '?target=...'.
- source_labels: [ctx]
target_label: __param_ctx # 'ctx' -> '?ctx=...'.
- source_labels: [client]
target_label: client # Label all probe's with client id.
- source_labels: [protocol]
target_label: protocol # Label all probe's with used protocol.
- source_labels: [__param_target]
target_label: target # Label all probe's with target.
- target_label: __address__
replacement: 127.0.0.1:9116 # The whitebox real hostname:port.- targets: [ "https://google.com" ]
labels:
ctx: "vless://c9f5228c-8870-47bd-a92f-9b38c7c02b08@1.2.3.4:443?type=tcp&encryption=none&security=reality&pbk=DF-3KL2W4RuNB2HgsEDmLqHLvvTTN4_QfwUCUn8Uhy0&fp=firefox&sni=ce-cdn.icloud-content.com&sid=dc8wq0b47450f9&spx=%2F&flow=xtls-rprx-vision#ring0-raii-idx0"
client: "ring0-raii-idx0" # Client unique identifier
protocol: "vless" # VPN protocol
# You can also add additional labels here:
# sni: "web.max.ru"
# And then, update relabel_configs in prometheus.yml job's config:
# - source_labels: [sni]
# target_label: sni
- targets: [ "https://cloudflare.com" ]
labels:
# Wireguard connection must be supplied as: base64-encoded peer .ini config prefixed with 'wireguard://'
ctx: "wireguard://W0ludGVyZmFjZV0KUHJpdmF0ZUtleSA9IFNObk5ON0l4YzV0ekNYS2FJNGZXNnEyOFYzbnhGS2YxcmNoYWt4bWdBbHM9CkFkZHJlc3MgPSAxMC4wLjAuMi8zMgpETlMgPSAxLjEuMS4xLCAxLjAuMC4xCk1UVSA9IDE0MjAKCiMgLTEKW1BlZXJdClB1YmxpY0tleSA9IHk2MTdkQ2dNM1g2bEtEanBkdDVhQ3dIWmROWW5OT0FwMFMyanFUbGpmZzA9CkFsbG93ZWRJUHMgPSAwLjAuMC4wLzAsIDo6LzAKRW5kcG9pbnQgPSAxLjIuMy40OjQ0Mw=="
client: "wg-raii-idx0"
protocol: "wireguard"
- targets: [ "https://google.com" ]
labels:
# AmneziaWG connection with obfuscation parameters (Jc, Jmin, Jmax, S1, S2, H1-H4)
# Must be supplied as: base64-encoded peer .ini config prefixed with 'awg://'
ctx: "awg://W0ludGVyZmFjZV0KUHJpdmF0ZUtleSA9IFNObk5ON0l4YzN0emxYS2FJNGY4NnEyOFYzbnhGS2YzcmNoYWt4bWdCbHM9CkFkZHJlc3MgPSAxMC4wLjAuMi8zMgpETlMgPSAxLjEuMS4xLCAxLjAuMC4xCk1UVSA9IDE0MjAKSmMgPSAzCkptaW4gPSA1MApKbWF4ID0gMTAwMApTMSA9IDIwClMyID0gNzgKSDEgPSAzOTEzMTI3OApIMiA9IDgzMjEzODE4NQpIMyA9IDE0MzY5NTc4NTcKSDQgPSAxNjM1ODc3NzQ2CgpbUGVlcl0KUHVibGljS2V5ID0geTYxN2RDZ00zWDZsS0RqcGR0NWFHY0FaZE5Zbk5PQXAwUzNqYVRsamZnMD0KQWxsb3dlZElQcyA9IDAuMC4wLjAvMCwgOjovMApFbmRwb2ludCA9IDEuMi4zLjQ6Mjc3ODkK"
client: "awg-raii-idx0"
protocol: "amneziawg"Tip
After all of that, reload Prometheus, visit http://localhost:9090/targets and check for your whitebox job.
While whitebox is designed with Prometheus integration in mind, it can also be used independently as a lightweight VPN probing tool.
You can configure periodic probes using a simple curl command in a cron job or any task scheduler of your choice.
*/5 * * * * curl -s "http://localhost:9116/probe?ctx=<urlencoded_vpn_uri>&target=google.com" >> /var/log/whitebox-probe.log 2>&1- VLESS XHTTP transport protocol support (#1).
- CI/CD for basic build/test workflow (#2).
- JSON Subscriptions VPN-url's support (#3).
- HTTP Roundtrip tracing w/ duration metrics (#4).
- AmneziaWG protocol support, thanks to @nsvk13 (#6).
- Whitebox YAML configuration w/ auto-reload by SIGHUP (#7).
- Response status/body validation.
- Custom HTTP-headers qualify support.
- Configuration environment variables interpolation support (#8).
- Hysteria2 + XHTTP extra settings support.
- Authorization/OAuth 2.0 support.
- Configuration for TLS protocol of HTTP probe support.
- More advanced metrics.
make buildmake runmake watchmake test
make test-raceSet WHITEBOX_EXPOSE_PPROF=1 to mount net/http/pprof under /debug/pprof:
WHITEBOX_EXPOSE_PPROF=1 make run
go tool pprof http://localhost:9116/debug/pprof/heapMIT License, see LICENSE.

